E.6 - Generatività

Obiettivo
Far generare un'animazione continua definendo il comportamento e le modalità di visualizzazione di qualche centinaio di agenti autonomi.
Modalità
Creare uno sketch in cui siano previste le seguenti operazioni:
- definizione di una o più classi di agenti autonomi con proprietà e metodi che ne consentano un uso grafico;
- creazione e riempimento (nel
setup()o progressivamente) di un array di agenti autonomi basati sulla classe definita; - creazione del codice che permetta l'evoluzione degli agenti e disegni immagini dipendenti dalle loro proprietà.
La risoluzione del canvas dovrà adattarsi a quella della finestra in cui viene eseguito lo sketch. Per l'adattamento dovranno essere usati le variabili windowWidth e windowHeight, il gestore dell'evento windowResized() e l'istruzione resizeCanvas(). Il gestore dell'evento windowResized() non dovrà essere tolto dallo sketch.
Il codice dev'essere strutturato in questo modo:
// DEFINIZIONE CLASSE/I
class Agente {
constructor() {
// Imposta le proprietà iniziali
}
display() {
// Disegna l'agente
}
update() {
// Aggiorna le proprietà
}
}
// DATI GLOBALI
let agenti = []; // array degli agenti
// Altre variabili globali
// INIZIALIZZAZIONI
function setup() {
createCanvas( windowWidth, windowHeight );
// Inizializzazioni variabili globali
restart();
}
function windowResized() {
resizeCanvas( windowWidth, windowHeight );
restart();
}
function restart() {
// Modifica variabili legate a dimensione canvas
}
// DISEGNO FOTOGRAMMA
function draw() {
// Eventuali controlli e creazione di altri agenti
// Aggiornamento + eliminazione o disegno degli agenti
for (let i = agenti.length-1; i >= 0; --i) { // dall'ultimo al primo...
agenti[i].update();
if (condizione eliminazione) { // se agente da eliminare...
agenti.splice(i, 1); // elimina
} else { // altrimenti...
agenti[i].display(); // disegna
}
}
}
- DEFINIZIONE CLASSE/I - è la parte più importante dell'esercitazione; possono essere aggiunti sia altre proprietà (se servono ulteriori dati diversi per ogni agente, ad esempio il suo colore) che altri metodi; nel metodo
display()vanno inserite le istruzioni di disegno dell'agente mentre nel metodoupdate()vanno elaborati i valori delle proprietà (ad esempio, i vettoriposeveled eventuali nuove proprietà); se sono necessari agenti con comportamenti molto diversi, è possibile definire due o più classi, con gli stessi metodi di base; - DATI GLOBALI - possono essere definite liberamente variabili globali con valori utili a tutti gli agenti ma l'array che contiene tutti gli agenti deve, ovviamente, rimanere;
- INIZIALIZZAZIONI - oltre a eventuali nuove inizializzazioni, nel
setup()può essere inserito un for() che crea subito tutti gli agenti [vedi le varianti successive] ma gli agenti possono essere aggiunti anche progressivamente neldraw(); per supportare meglio il ridimensionamento della finestra conviene definire una funzione (ad esempiorestart()) con le istruzioni per reimpostare le variabili dipendenti dalle dimensioni del canvas; - DISEGNO FOTOGRAMMA - il
draw()può generare l'immagine visualizzando i singoli agenti oppure usandone le proprietà per disegnare figure dipendenti dall'evoluzione del sistema; oltre a far aggiornare le proprietà degli agenti, chiamando il metodoupdate(), ildraw()può essere utilizzato per fare controlli o modifiche all'array degli agenti.
Sul Web Editor di p5.js si può trovare uno sketch di base da cui partire.
Consigli
La parte su cui conviene iniziare a ragionare è il comportamento degli agenti determinato dal codice del metodo update() della classe Agente. In relazione al comportamento può poi essere necessario definire nuove proprietà nel metodo constructor(). Solo alla fine conviene scegliere le istruzioni grafiche più adatte a valorizzare le possibilità grafiche dell'agente intervenendo nel metodo display(). Ovviamente possono esserci aggiustamenti successivi alle parti di codice già definite ma è meglio partire dai metodi della classe.
Le modifiche al setup(), all'eventuale restart() e al draw() sono le meno importanti per le finalità di questa esercitazione e servono solo a gestire la creazione, l'aggiornamento e la visualizzazione di tutti gli "agenti".
Aggiunta di una proprietà
Può essere utile aggiungere una proprietà alla classe per memorizzare una caratteristica geometrica o cromatica con valori specifici per ogni istanza.
Si ricorda che per farlo è necessario:
- definire la proprietà nel costruttore;
- usarla nel metodo
display(); - aggiornarla (se deve cambiare) nel metodo
update().
Ad esempio, può essere aggiunto il colore di riempimento dell'agente:
class Agente {
constructor() {
...
this.rosso = random(255); // definizione
}
display() {
fill( this.rosso, 128, 64 ); // uso
circle( this.pos.x, this.pos.y, 5 );
}
update() {
...
this.rosso = (this.rosso + 1) % 255; // aggiornamento interno
}
}
La proprietà di ogni singola istanza può essere modificata dall'esterno della classe, solitamente nel draw():
agenti[i].rosso = random(64,192); // aggiornamento esterno
Visualizzazione dell'agente
Il metodo display() può limitarsi a contenere una sola istruzione di disegno, soprattutto se gli attributi grafici (contorni e colori) sono uguali per tutte le istanze. In alcuni casi, ad esempio se gli attributi grafici sono proprietà della classe, può essere utile inserire sempre almeno le istruzioni fill() e stroke(). In altri casi, ad esempio se devono essere fatte delle trasformazioni geometriche, conviene usare anche le istruzioni push() e pop().
Un agente costituito da un segmento di 50 pixel allineato alla direzione di spostamento potrebbe avere il metodo display() definito in questo modo:
display() {
push();
translate( this.pos );
rotate( this.vel.heading() ); // allinea a direzione spostamento
stroke( 128 );
line( -50, 0, 0, 0 ); // segmento da sinistra verso il centro
pop();
}
Anche i vertici delle figure chiuse possono essere indicati con le coordinate relative al centro di rotazione:
triangle( -40, -5, 0, 0, -40, 5 ); // triangolo >+ centro
Comportamento dell'agente
Di solito il comportamento viene implementato modificando progressivamente alcune proprietà ma può essere necessario fare delle verifiche per evitare che i valori finiscano per far sparire l'agente. Per far muovere un agente facendogli compiere ogni tanto una leggera rotazione e per farlo rientrare dal lato opposto del canvas quando esce, il metodo update() potrebbe essere strutturato in questo modo:
update() {
// MODIFICA LE PROPRIETÀ
if (random() < 0.01) { // con una probabilità dell'1%...
this.vel.rotate(random(-0.1, 0.1)); // cambia orientamento spostamento
}
this.pos.add(this.vel); // sposta l'agente
// VERIFICA E "CORREGGI"
if (this.pos.x < 0) { // se esce a sinistra...
this.pos.x += width; // rientra da destra
} else if (this.pos.x > width) { // se esce a destra...
this.pos.x -= width; // rientra da sinistra
}
if (this.pos.y < 0) { // se esce in alto...
this.pos.y += height; // rientra dal basso
} else if (this.pos.y > height) { // se esce in basso...
this.pos.y -= height; // rientra dall'alto
}
}
Sul Web Editor si può trovare una variante "pittorica" che implementa un comportamento un po' diverso.
Nascita degli agenti
Le istanze degli agenti possono essere create tutte insieme nel setup(). Ad esempio, se se ne vogliono creare 300 posizionate a caso nel canvas, si può usare un for come questo:
for (let i=0; i < 300; i++) { // per 300 volte...
let pos = createVector(random(width),random(height));
agenti[i] = new Agente( pos ); // crea un'istanza e memorizzala nell'array
}
Il riempimento del'array può essere anche progressivo e le istanze possono essere create nel draw() o in una delle funzioni di gestione degli eventi della tastiera, del mouse, ecc. Ad esempio, se si vogliono far creare le istanze mentre si tiene premuto il tasto del mouse, si può inserire un codice come questo nel draw():
if (mouseIsPressed) {
let mousePos = createVector(mouseX, mouseY);
agenti.unshift(new Agente(mousePos)); // aggiungi in cima all'array
}
Sempre nella variante "pittorica" si può trovare un esempio pratico di questo codice.
Morte degli agenti
L'eventuale "morte" delle istanze degli agenti è gestita dall'if del for finale nel draw(). La condizione può riguardare il numero massimo di istanze ma anche una proprietà interna alla classe. Ad esempio, potrebbe essere previsto un contatore per tenere traccia dei fotogrammi passati dalla creazione dell'istanza:
class Agente {
constructor() {
...
this.vita = 200; // vita iniziale, in fotogrammi
}
...
update() {
...
this.vita--; // riduzione vita
}
}
Nel draw() si potrebbe quindi controllare se il contatore di ogni singola istanza si è azzerato ed è quindi necessario toglierla dall'array agenti:
function draw() {
...
// Aggiornamento + eliminazione o disegno degli agenti
for (let i = agenti.length-1; i >= 0; --i) {
agenti[i].update();
if (agenti[i].vita < 0) { // se "vita" esaurita...
agenti.splice(i, 1); // elimina istanza
} else { // altrimenti...
agenti[i].display(); // disegna istanza
}
}
}
Ancora nella variante "pittorica" si può vedere un esempio con agenti dalla "vita" limitata.
Relazioni fra agenti
Per prevedere azioni dipendenti dalle relazioni fra gli agenti, è possibile usare due for annidati nel draw() che confrontino ogni agente con tutti gli altri. Utilizzando le proprietà pos, ad esempio, può essere calcolata la distanza fra i due agenti e con un if si possono definire le azioni da eseguire se la distanza e superiore o inferiore a un determinato valore. Le proprietà possono essere utilizzate anche per disegnare elementi esterni come, ad esempio, una linea che unisce i due agenti. Il codice di base potrebbe essere questo:
function draw() {
// ...
// CONFRONTA AGENTI ED ESEGUI EVENTUALI ISTRUZIONI
for (let a = 0; a < agenti.length-1; ++a) {
let agenteA = agenti[a];
for (let b = a+1; b < agenti.length; ++b) {
let agenteB = agenti[b];
let distanza = agenteA.pos.dist( agenteB.pos );
// modifica proprietà di agenteA e agenteB e/o
// disegna elementi esterni agli agenti
}
}
for (let agente of agenti) {
agente.display();
agente.update();
}
}
Sul Web Editor si può trovare uno sketch d'esempio.
Altre varianti dello sketch di base
Sempre sul Web Editor sono presenti anche tre versioni dello sketch di base che mostrano alcune tecniche utilizzabili per l'esercitazione:
- creazione iniziale degli agenti;
- rimbalzo sui lati del canvas;
- disegno di linee di connessione fra coppie di agenti;
- dissolvenza dei fotogrammi precedenti;
- creazione iniziale degli agenti;
- uso di elementi (tipografici) orientati verso la direzione di spostamento;
- rientro degli agenti dal lato opposto a quello di uscita;
- confronto fra le proprietà di tutti gli agenti.
- uso di due classi diverse;
- creazione iniziale degli agenti;
- movimenti perpendicolari
- confronto fra agenti e notifica della distanza (metodo
approach()delle classi).
Consegna
Lo sketch andrà consegnato, dopo l'aggiunta dell'anteprima (obbligatoria), seguendo le modalità indicate nel "compito" della Classroom del corso.
Prima di consegnare l'esercitazione si consiglia di verificare il caricamento dell'anteprima attraverso la pagina di verifica dello sketch e l'eventuale adattamento del canvas alla modifica delle dimensioni della finestra del browser.